"""
Call graph analyzer for TypeScript code.

Analyzes function calls, method invocations, and builds call graphs for TypeScript
code structures including classes, interfaces, and async patterns.
"""

from typing import Dict, List, Any, Optional, Set, Tuple
from collections import defaultdict, deque
import re

from ...core.error_handler import AnalysisError, log_info


class TypeScriptCallGraphAnalyzer:
    """Analyzes TypeScript code to build call graphs and understand dependencies."""
    
    def __init__(self):
        """Initialize the TypeScript call graph analyzer."""
        self.call_cache = {}
        self.visited_functions = set()
        self.interface_methods = {}
        self.class_hierarchy = {}
    
    def analyze_function_calls(self, function_info: Dict[str, Any], 
                              analysis: Dict[str, Any], max_depth: int = 10) -> Dict[str, Any]:
        """Analyze function calls starting from a specific function."""
        self.call_cache.clear()
        self.visited_functions.clear()
        
        # Build lookup tables
        function_lookup = {f['name']: f for f in analysis.get('functions', [])}
        method_lookup = self._build_method_lookup(analysis.get('classes', []))
        interface_lookup = self._build_interface_lookup(analysis.get('interfaces', []))
        
        # Start call graph generation
        call_graph = self._build_function_call_graph(
            function_info, function_lookup, method_lookup, interface_lookup, max_depth, 0
        )
        
        return {
            'root_function': function_info['name'],
            'graph': call_graph,
            'total_calls': self._count_calls(call_graph),
            'max_depth': self._calculate_max_depth(call_graph),
            'unique_functions': list(self.visited_functions),
            'async_patterns': self._analyze_async_patterns(call_graph),
            'statistics': self._generate_statistics(call_graph)
        }
    
    def _build_method_lookup(self, classes: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
        """Build lookup table for class methods."""
        method_lookup = {}
        
        for class_info in classes:
            class_name = class_info['name']
            self.class_hierarchy[class_name] = {
                'extends': class_info.get('extends', []),
                'implements': class_info.get('implements', [])
            }
            
            for method in class_info.get('methods', []):
                method_key = f"{class_name}.{method['name']}"
                method_lookup[method_key] = method
        
        return method_lookup
    
    def _build_interface_lookup(self, interfaces: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
        """Build lookup table for interface methods."""
        interface_lookup = {}
        
        for interface_info in interfaces:
            interface_name = interface_info['name']
            
            for method in interface_info.get('methods', []):
                method_key = f"{interface_name}.{method['name']}"
                interface_lookup[method_key] = method
        
        return interface_lookup
    
    def _build_function_call_graph(self, function_info: Dict[str, Any], 
                                  function_lookup: Dict[str, Dict[str, Any]],
                                  method_lookup: Dict[str, Dict[str, Any]],
                                  interface_lookup: Dict[str, Dict[str, Any]],
                                  max_depth: int, current_depth: int) -> Dict[str, Any]:
        """Build call graph recursively for a function."""
        if current_depth >= max_depth:
            return {'name': function_info['name'], 'calls': []}
        
        function_name = function_info['name']
        if function_name in self.call_cache:
            return self.call_cache[function_name]
        
        self.visited_functions.add(function_name)
        
        # Get function calls from the function
        calls_made = function_info.get('calls', [])
        call_nodes = []
        
        for call in calls_made:
            call_name = call.get('name', '')
            call_type = call.get('type', 'function')
            
            # Try to resolve the call
            target_function = None
            
            if call_type == 'method':
                # Handle method calls (class.method or interface.method)
                if '.' in call_name:
                    target_function = method_lookup.get(call_name) or interface_lookup.get(call_name)
                else:
                    # Try to find method in class hierarchy
                    target_function = self._resolve_method_call(call_name, method_lookup)
            else:
                # Handle function calls
                target_function = function_lookup.get(call_name)
            
            if target_function:
                # Recursively build call graph
                call_node = self._build_function_call_graph(
                    target_function, function_lookup, method_lookup, interface_lookup,
                    max_depth, current_depth + 1
                )
                call_node.update({
                    'type': call_type,
                    'async': call.get('async', False),
                    'line': call.get('line', 0)
                })
                call_nodes.append(call_node)
            else:
                # External or unresolved call
                call_nodes.append({
                    'name': call_name,
                    'type': call_type,
                    'external': True,
                    'async': call.get('async', False),
                    'line': call.get('line', 0),
                    'calls': []
                })
        
        result = {
            'name': function_name,
            'type': function_info.get('type', 'function'),
            'async': function_info.get('async', False),
            'line': function_info.get('line', 0),
            'calls': call_nodes
        }
        
        self.call_cache[function_name] = result
        return result
    
    def _resolve_method_call(self, method_name: str, method_lookup: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        """Resolve method call through class hierarchy."""
        for class_method_key, method_info in method_lookup.items():
            if class_method_key.endswith(f".{method_name}"):
                return method_info
        return None
    
    def _count_calls(self, graph: Dict[str, Any]) -> int:
        """Count total number of calls in the graph."""
        count = len(graph.get('calls', []))
        for call in graph.get('calls', []):
            count += self._count_calls(call)
        return count
    
    def _calculate_max_depth(self, graph: Dict[str, Any]) -> int:
        """Calculate maximum depth of the call graph."""
        if not graph.get('calls'):
            return 1
        
        max_call_depth = 0
        for call in graph.get('calls', []):
            call_depth = self._calculate_max_depth(call)
            max_call_depth = max(max_call_depth, call_depth)
        
        return 1 + max_call_depth
    
    def _analyze_async_patterns(self, graph: Dict[str, Any]) -> Dict[str, Any]:
        """Analyze async/await patterns in the call graph."""
        async_functions = []
        promise_chains = []
        await_calls = []
        
        self._collect_async_info(graph, async_functions, promise_chains, await_calls)
        
        return {
            'async_functions': async_functions,
            'promise_chains': promise_chains,
            'await_calls': await_calls,
            'total_async': len(async_functions)
        }
    
    def _collect_async_info(self, node: Dict[str, Any], async_functions: List[str], 
                           promise_chains: List[str], await_calls: List[str]):
        """Recursively collect async pattern information."""
        if node.get('async'):
            async_functions.append(node['name'])
        
        # Check for promise patterns
        if any('promise' in call['name'].lower() for call in node.get('calls', [])):
            promise_chains.append(node['name'])
        
        # Check for await calls
        if any(call.get('async') for call in node.get('calls', [])):
            await_calls.append(node['name'])
        
        for call in node.get('calls', []):
            self._collect_async_info(call, async_functions, promise_chains, await_calls)
    
    def _generate_statistics(self, graph: Dict[str, Any]) -> Dict[str, Any]:
        """Generate statistics for the call graph."""
        function_types = defaultdict(int)
        call_patterns = defaultdict(int)
        depth_distribution = defaultdict(int)
        
        self._collect_statistics(graph, function_types, call_patterns, depth_distribution, 1)
        
        return {
            'function_types': dict(function_types),
            'call_patterns': dict(call_patterns),
            'depth_distribution': dict(depth_distribution),
            'external_calls': sum(1 for call in self._get_all_calls(graph) if call.get('external')),
            'async_ratio': len([f for f in self.visited_functions if 'async' in f]) / len(self.visited_functions) if self.visited_functions else 0
        }
    
    def _collect_statistics(self, node: Dict[str, Any], function_types: Dict[str, int],
                           call_patterns: Dict[str, int], depth_distribution: Dict[str, int], depth: int):
        """Recursively collect statistics from the call graph."""
        function_types[node.get('type', 'function')] += 1
        depth_distribution[depth] += 1
        
        for call in node.get('calls', []):
            call_type = call.get('type', 'function')
            if call.get('external'):
                call_patterns[f"external_{call_type}"] += 1
            else:
                call_patterns[call_type] += 1
            
            self._collect_statistics(call, function_types, call_patterns, depth_distribution, depth + 1)
    
    def _get_all_calls(self, graph: Dict[str, Any]) -> List[Dict[str, Any]]:
        """Get all calls from the graph recursively."""
        calls = []
        for call in graph.get('calls', []):
            calls.append(call)
            calls.extend(self._get_all_calls(call))
        return calls


class TypeScriptDependencyAnalyzer:
    """Analyzes TypeScript dependencies and module relationships."""
    
    def __init__(self):
        """Initialize the dependency analyzer."""
        self.module_graph = {}
        self.circular_deps = []
    
    def analyze_dependencies(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
        """Analyze module dependencies and relationships."""
        modules = analysis.get('modules', [])
        imports = analysis.get('imports', [])
        
        # Build module dependency graph
        self.module_graph = self._build_module_graph(modules, imports)
        
        # Find circular dependencies
        self.circular_deps = self._find_circular_dependencies()
        
        return {
            'module_graph': self.module_graph,
            'circular_dependencies': self.circular_deps,
            'dependency_levels': self._calculate_dependency_levels(),
            'external_dependencies': self._identify_external_dependencies(imports),
            'statistics': self._generate_dependency_statistics()
        }
    
    def _build_module_graph(self, modules: List[Dict[str, Any]], 
                           imports: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
        """Build module dependency graph."""
        graph = {}
        
        # Initialize modules
        for module in modules:
            module_name = module['name']
            graph[module_name] = {
                'imports': [],
                'exports': module.get('exports', []),
                'path': module.get('path', ''),
                'type': module.get('type', 'module')
            }
        
        # Add import relationships
        for import_info in imports:
            from_module = import_info.get('from_module', '')
            to_module = import_info.get('module', '')
            
            if from_module in graph:
                graph[from_module]['imports'].append({
                    'module': to_module,
                    'items': import_info.get('items', []),
                    'type': import_info.get('type', 'named')
                })
        
        return graph
    
    def _find_circular_dependencies(self) -> List[List[str]]:
        """Find circular dependencies in the module graph."""
        visited = set()
        recursion_stack = set()
        cycles = []
        
        def dfs(module: str, path: List[str]):
            if module in recursion_stack:
                # Found a cycle
                cycle_start = path.index(module)
                cycles.append(path[cycle_start:] + [module])
                return
            
            if module in visited:
                return
            
            visited.add(module)
            recursion_stack.add(module)
            path.append(module)
            
            for import_info in self.module_graph.get(module, {}).get('imports', []):
                imported_module = import_info['module']
                if imported_module in self.module_graph:
                    dfs(imported_module, path.copy())
            
            recursion_stack.remove(module)
        
        for module in self.module_graph:
            if module not in visited:
                dfs(module, [])
        
        return cycles
    
    def _calculate_dependency_levels(self) -> Dict[str, int]:
        """Calculate dependency levels (distance from leaf modules)."""
        levels = {}
        
        def calculate_level(module: str, visited: Set[str]) -> int:
            if module in visited:
                return 0  # Circular dependency
            if module in levels:
                return levels[module]
            
            visited.add(module)
            module_info = self.module_graph.get(module, {})
            
            max_level = 0
            for import_info in module_info.get('imports', []):
                imported_module = import_info['module']
                if imported_module in self.module_graph:
                    level = calculate_level(imported_module, visited.copy())
                    max_level = max(max_level, level)
            
            levels[module] = max_level + 1
            return levels[module]
        
        for module in self.module_graph:
            calculate_level(module, set())
        
        return levels
    
    def _identify_external_dependencies(self, imports: List[Dict[str, Any]]) -> List[str]:
        """Identify external (npm) dependencies."""
        external_deps = set()
        
        for import_info in imports:
            module = import_info.get('module', '')
            # External modules typically don't start with './' or '../'
            if not module.startswith('.') and module not in self.module_graph:
                external_deps.add(module)
        
        return list(external_deps)
    
    def _generate_dependency_statistics(self) -> Dict[str, Any]:
        """Generate dependency statistics."""
        total_modules = len(self.module_graph)
        total_imports = sum(len(module_info.get('imports', []))
                           for module_info in self.module_graph.values())
        
        return {
            'total_modules': total_modules,
            'total_imports': total_imports,
            'circular_dependencies': len(self.circular_deps),
            'average_imports_per_module': total_imports / total_modules if total_modules > 0 else 0,
            'modules_with_no_imports': sum(1 for module_info in self.module_graph.values()
                                         if not module_info.get('imports')),
            'max_imports_in_module': max((len(module_info.get('imports', []))
                                        for module_info in self.module_graph.values()), default=0)
        }